Vue 3.0 Props的初始化和更新流程的细节分析

您所在的位置:网站首页 vue props传值无法更新 Vue 3.0 Props的初始化和更新流程的细节分析

Vue 3.0 Props的初始化和更新流程的细节分析

2023-11-20 12:13| 来源: 网络整理| 查看: 265

Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

Vue.js可以让组件的使用者在组件外部传递props参数,组件拿到这些props的值来实现各种各样的功能。本文我们就来探讨下组件props的初始化和更新流程。

在前一篇文章中,我们知道setup函数的第一个参数是props,本文我们就来了解下props是如何初始化和更新的。

在开始之前我们先弄清两个概念:

Props配置:就是编写组件时写的props属性,描述一个组件的Props的数据类型和默认值等信息。例如组件定义时:props: ['msg']

Props数据:是在使用组件时给组件传递的数据。例如组件使用时:

Props的初始化流程 normalizePropsOptions进行Props标准化配置

挂载组件的第一步是调用createComponentInstance来创建组件实例对象,初始化的过程中就实现了标准化props的配置normalizePropsOptions方法:

const instance: ComponentInternalInstance = { // 省略... propsOptions: normalizePropsOptions(type, appContext), // 省略... return instance }

接下来我们就来看看标准化Props配置normalizePropsOptions方法的具体逻辑。

export function normalizePropsOptions( comp: ConcreteComponent, appContext: AppContext, asMixin = false ): NormalizedPropsOptions { // 1. const cache = appContext.propsCache const cached = cache.get(comp) if (cached) { return cached } const raw = comp.props const normalized: NormalizedPropsOptions[0] = {} const needCastKeys: NormalizedPropsOptions[1] = [] // 2. let hasExtends = false if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) { const extendProps = (raw: ComponentOptions) => { if (__COMPAT__ && isFunction(raw)) { raw = raw.options } hasExtends = true const [props, keys] = normalizePropsOptions(raw, appContext, true) extend(normalized, props) if (keys) needCastKeys.push(...keys) } if (!asMixin && appContext.mixins.length) { appContext.mixins.forEach(extendProps) } if (comp.extends) { extendProps(comp.extends) } if (comp.mixins) { comp.mixins.forEach(extendProps) } } if (!raw && !hasExtends) { cache.set(comp, EMPTY_ARR as any) return EMPTY_ARR as any } // 3 if (isArray(raw)) { for (let i = 0; i < raw.length; i++) { const normalizedKey = camelize(raw[i]) if (validatePropName(normalizedKey)) { normalized[normalizedKey] = EMPTY_OBJ } } } else if (raw) { // 4 for (const key in raw) { const normalizedKey = camelize(key) if (validatePropName(normalizedKey)) { const opt = raw[key] const prop: NormalizedProp = (normalized[normalizedKey] = isArray(opt) || isFunction(opt) ? { type: opt } : opt) if (prop) { const booleanIndex = getTypeIndex(Boolean, prop.type) const stringIndex = getTypeIndex(String, prop.type) prop[BooleanFlags.shouldCast] = booleanIndex > -1 prop[BooleanFlags.shouldCastTrue] = stringIndex < 0 || booleanIndex < stringIndex // if the prop needs boolean casting or default value if (booleanIndex > -1 || hasOwn(prop, 'default')) { needCastKeys.push(normalizedKey) } } } } } // 5 const res: NormalizedPropsOptions = [normalized, needCastKeys] cache.set(comp, res) return res }

标准化Props配置的代码解释:

先从appContext.propsCache中去获取组件对象为key的配置缓存,如果取到了直接返回缓存结果; 再处理extends和mixins中的Props属性,他们二者的作用是扩展组件的定义,所以需要递归他们定义的Props执行normalizePropsOptions方法,然后将结果放在组件的存储结果中。(从处理来看我们知道extends只能有一个并被优先处理,mixins可以有多个) 如果Props配置是数组且每个元素是个字符串,则将字符串改为驼峰命名,并我每个key创建一个空对象. 定义:props: ['age','message-id'] 结果:propOptions: {age: {}, messageId: {}} 如果Props配置是对象,则标准化每个不是以$开头的prop的定义。首先把数组和函数转换成对象:{type: prop}。然后判断如果prop的type属性中有定义Boolean,则标记为需要转换数据;如果prop的type属性中Boolean存在,String不存在或者Boolean在String前面,此时标记为需要转换成boolean类型。 定义:props: {name: String, intro: [Boolean, String]} 结果:propOptions.normalized:

normalized

5. 如果prop含有default或者类型包含了Boolean,则标记为key的值需要转换,放在needCastKeys中。

propOptions

将获取到的propOptions缓存到appContext.propsCache中。 initProps设置Props初始化

我们前面的章节有提到在setupComponent设置组件对象的时候会调用initProps(instance, props, isStateful, isSSR)进行Props初始化处理。我们接下来就来介绍下它:

setupComponent

initProps的代码逻辑如下:

initProps

initProps初始化的过程分为四个步骤:

给Props进行设值; 没有传值的Props将其值设置为undefined; 如果是开发环境,进行Props的验证,给出错误提示; 将props变为响应式数据赋值给组件实例对象,将attrs赋值给组件实例对象。

我们接下来逐步分析。

setFullProps设置props的值

setFullProps

为了方便理解,在此给个例子:

demo

设置的流程解释:

遍历rawProps的值,例子中就是{name: "Lan", address: "北京东城", age: "18", intro: ""}; 如果propsOptions中有rawProps对应的key,如果不需要转换,就直接赋值给props,如果需要转换值,则先暂存到rawCastValues中;如果propsOptions中没有rawProps对应的key,并且不是事件相关的属性,则将其存到attrs中。 props = {address: "北京东城"}; attrs = {age: 18}; rawCastValues = {intro: "", name: "Lan"}; 通过resolvePropValue进行转换: 如果有default且父组件没有传递值的情况下直接使用默认值,否则用传入的值(例子中的name) 如果key[0] === true,如果父组件没有传值且没有默认值就设置为false, 如果父组件有传值,这种情况下如果key[1] === true且字符串为空,这设置为true(例子中的intro),否则为false。 props = {address: "北京东城",intro: true,name: "Lan"}; attrs = {age: 18}; validateProps验证props值的合法性

validateProps

验证合法性主要有如下几个规则:

如果必须传值,但是没有传值就报警告 如果不必须传值,传值为null, 有效返回 如果类型不匹配就报警告 如果validator, 传入的值验证不通过报警告 props设置成浅响应式对象和赋值 instance.props = shallowReactive(props) instance.props = props instance.attrs = attrs

这一步很好理解。shallowReactive表示只监测props的变化,内部属性的变化不会被监测。

留个小问题:props为什么要设置成响应式呢?

至此,prop的初始化就完成了。在后面的执行setup函数中就能将props对象传递给子组件了。

Props更新流程 更新Props触发的渲染流程

Props是在父组件定义然后传给子组件的,所以Props值的变化会父组件的重新渲染。父组件的重新渲染会触发patch,然后当更新子组件的时候触发updateComponent流程,由于props变化了,hasPropsChanged会返回true,也就是说shouldUpdateComponent为true,此时给子组件的next设置为新的VNode,然后执行子组件的重新渲染。

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => { const instance = (n2.component = n1.component)! if (shouldUpdateComponent(n1, n2, optimized)) { // normal update instance.next = n2 // in case the child component is also queued, remove it to avoid // double updating the same child component in the same flush. invalidateJob(instance.update) // instance.update is the reactive effect. instance.update() } } export function shouldUpdateComponent( prevVNode: VNode, nextVNode: VNode, optimized?: boolean ): boolean { const { props: prevProps, children: prevChildren, component } = prevVNode const { props: nextProps, children: nextChildren, patchFlag } = nextVNode // 省略... return hasPropsChanged(prevProps, nextProps, emits) }

当子组件重新渲染的时候会执行componentUpdateFn方法,且此时新的VNode的next有值,触发的 updateComponentPreRender流程中会调用updateProps方法更新子组件实例对象的props。

const updateComponentPreRender = ( instance: ComponentInternalInstance, nextVNode: VNode, optimized: boolean ) => { nextVNode.component = instance const prevProps = instance.vnode.props instance.vnode = nextVNode instance.next = null // 更新props updateProps(instance, nextVNode.props, prevProps, optimized) }

更新props后再执行对subTree VNode执行patch进行更新子组件。

字不如图

这里我们回过头来看看pros是如何更新的。

updateProps更新的具体细节

它的主要目标就是把父组件渲染时得到的新值更新到子组件的实例对象的props中。

在Vue的编译阶段可以知道组件的VNode的PatchFlags:

如果是PatchFlags是PatchFlags.PROPS, 则只更新vnode.dynamicProps`即动态props的值。 如果当前组件的PatchFlags是PatchFlags.FULL_PROPS, 这时候先执行setFullProps流程,然后删除掉不再使用的动态props的值。

这里把可略过的代码附上:

export function updateProps( instance: ComponentInternalInstance, rawProps: Data | null, rawPrevProps: Data | null, optimized: boolean ) { const { props, attrs, vnode: { patchFlag } } = instance const rawCurrentProps = toRaw(props) const [options] = instance.propsOptions let hasAttrsChanged = false if ( // always force full diff in dev // - #1942 if hmr is enabled with sfc component // - vite#872 non-sfc component used by sfc component !( __DEV__ && (instance.type.__hmrId || (instance.parent && instance.parent.type.__hmrId)) ) && (optimized || patchFlag > 0) && !(patchFlag & PatchFlags.FULL_PROPS) ) { if (patchFlag & PatchFlags.PROPS) { // Compiler-generated props & no keys change, just set the updated // the props. const propsToUpdate = instance.vnode.dynamicProps! for (let i = 0; i < propsToUpdate.length; i++) { let key = propsToUpdate[i] // PROPS flag guarantees rawProps to be non-null const value = rawProps![key] if (options) { // attr / props separation was done on init and will be consistent // in this code path, so just check if attrs have it. if (hasOwn(attrs, key)) { if (value !== attrs[key]) { attrs[key] = value hasAttrsChanged = true } } else { const camelizedKey = camelize(key) props[camelizedKey] = resolvePropValue( options, rawCurrentProps, camelizedKey, value, instance, false /* isAbsent */ ) } } else { if (__COMPAT__) { if (isOn(key) && key.endsWith('Native')) { key = key.slice(0, -6) // remove Native postfix } else if (shouldSkipAttr(key, instance)) { continue } } if (value !== attrs[key]) { attrs[key] = value hasAttrsChanged = true } } } } } else { // full props update. if (setFullProps(instance, rawProps, props, attrs)) { hasAttrsChanged = true } // in case of dynamic props, check if we need to delete keys from // the props object let kebabKey: string for (const key in rawCurrentProps) { if ( !rawProps || // for camelCase (!hasOwn(rawProps, key) && // it's possible the original props was passed in as kebab-case // and converted to camelCase (#955) ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey))) ) { if (options) { if ( rawPrevProps && // for camelCase (rawPrevProps[key] !== undefined || // for kebab-case rawPrevProps[kebabKey!] !== undefined) ) { props[key] = resolvePropValue( options, rawCurrentProps, key, undefined, instance, true /* isAbsent */ ) } } else { delete props[key] } } } if (attrs !== rawCurrentProps) { for (const key in attrs) { if (!rawProps || !hasOwn(rawProps, key)) { delete attrs[key] hasAttrsChanged = true } } } } } 提出一个问题

在传递动态的数据时,我们会在属性前面加上冒号:, 譬如下面的:address="address", 这个冒号:的作用是什么?我们进行标准化Props配置时候是对address做的处理时为什么没有这个冒号:做处理呢?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3